/*!
 * jQuery Mobile Navigator @VERSION
 * http://jquerymobile.com
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

//>>label: Navigation Manager
//>>group: Navigation
//>>description: Manages URL history and information in conjunction with the navigate event

( function( factory ) {
	if ( typeof define === "function" && define.amd ) {

		// AMD. Register as an anonymous module.
		define( [
			"jquery",
			"./../ns",
			"../events/navigate",
			"./path",
			"./history" ], factory );
	} else {

		// Browser globals
		factory( jQuery );
	}
} )( function( $ ) {

var path = $.mobile.path,
	initialHref = location.href;

$.mobile.Navigator = function( history ) {
	this.history = history;
	this.ignoreInitialHashChange = true;

	$.mobile.window.bind( {
		"popstate.history": $.proxy( this.popstate, this ),
		"hashchange.history": $.proxy( this.hashchange, this )
	} );
};

$.extend( $.mobile.Navigator.prototype, {
	historyEntryId: 0,
	squash: function( url, data ) {
		var state, href,
			hash = path.isPath( url ) ? path.stripHash( url ) : url;

		href = path.squash( url );

		// make sure to provide this information when it isn't explicitly set in the
		// data object that was passed to the squash method
		state = $.extend( {
			id: ++this.historyEntryId,
			hash: hash,
			url: href
		}, data );

		// replace the current url with the new href and store the state
		// Note that in some cases we might be replacing an url with the
		// same url. We do this anyways because we need to make sure that
		// all of our history entries have a state object associated with
		// them. This allows us to work around the case where $.mobile.back()
		// is called to transition from an external page to an embedded page.
		// In that particular case, a hashchange event is *NOT* generated by the browser.
		// Ensuring each history entry has a state object means that onPopState()
		// will always trigger our hashchange callback even when a hashchange event
		// is not fired.
		//
		//  Check is to make sure that jqm doesn't throw type errors in environments where
		//  window.history is not available (e.g. Chrome packaged apps)
		if ( window.history && window.history.replaceState ) {
			window.history.replaceState( state, state.title || document.title, href );
		}

		// If we haven't yet received the initial popstate, we need to update the reference
		// href so that we compare against the correct location
		if ( this.ignoreInitialHashChange ) {
			initialHref = href;
		}

		return state;
	},

	hash: function( url, href ) {
		var parsed, loc, hash, resolved;

		// Grab the hash for recording. If the passed url is a path
		// we used the parsed version of the squashed url to reconstruct,
		// otherwise we assume it's a hash and store it directly
		parsed = path.parseUrl( url );
		loc = path.parseLocation();

		if ( loc.pathname + loc.search === parsed.pathname + parsed.search ) {
			// If the pathname and search of the passed url is identical to the current loc
			// then we must use the hash. Otherwise there will be no event
			// eg, url = "/foo/bar?baz#bang", location.href = "http://example.com/foo/bar?baz"
			hash = parsed.hash ? parsed.hash : parsed.pathname + parsed.search;
		} else if ( path.isPath( url ) ) {
			resolved = path.parseUrl( href );
			// If the passed url is a path, make it domain relative and remove any trailing hash
			hash = resolved.pathname + resolved.search + ( path.isPreservableHash( resolved.hash ) ? resolved.hash.replace( "#", "" ) : "" );
		} else {
			hash = url;
		}

		return hash;
	},

	// TODO reconsider name
	go: function( url, data, noEvents ) {
		var state, href, hash, popstateEvent,
			isPopStateEvent = $.event.special.navigate.isPushStateEnabled();

		// Get the url as it would look squashed on to the current resolution url
		href = path.squash( url );

		// sort out what the hash sould be from the url
		hash = this.hash( url, href );

		// Here we prevent the next hash change or popstate event from doing any
		// history management. In the case of hashchange we don't swallow it
		// if there will be no hashchange fired (since that won't reset the value)
		// and will swallow the following hashchange
		if ( noEvents && hash !== path.stripHash( path.parseLocation().hash ) ) {
			this.preventNextHashChange = noEvents;
		}

		// IMPORTANT in the case where popstate is supported the event will be triggered
		//      directly, stopping further execution - ie, interupting the flow of this
		//      method call to fire bindings at this expression. Below the navigate method
		//      there is a binding to catch this event and stop its propagation.
		//
		//      We then trigger a new popstate event on the window with a null state
		//      so that the navigate events can conclude their work properly
		//
		// if the url is a path we want to preserve the query params that are available on
		// the current url.
		this.preventHashAssignPopState = true;
		window.location.hash = hash;

		// If popstate is enabled and the browser triggers `popstate` events when the hash
		// is set (this often happens immediately in browsers like Chrome), then the
		// this flag will be set to false already. If it's a browser that does not trigger
		// a `popstate` on hash assignement or `replaceState` then we need avoid the branch
		// that swallows the event created by the popstate generated by the hash assignment
		// At the time of this writing this happens with Opera 12 and some version of IE
		this.preventHashAssignPopState = false;

		state = $.extend( {
			url: href,
			hash: hash,
			title: document.title
		}, data );

		if ( isPopStateEvent ) {
			popstateEvent = new $.Event( "popstate" );
			popstateEvent.originalEvent = new $.Event( "popstate", { state: null } );

			state.id = ( this.squash( url, state ) || {} ).id;

			// Trigger a new faux popstate event to replace the one that we
			// caught that was triggered by the hash setting above.
			if ( !noEvents ) {
				this.ignorePopState = true;
				$.mobile.window.trigger( popstateEvent );
			}
		}

		// record the history entry so that the information can be included
		// in hashchange event driven navigate events in a similar fashion to
		// the state that's provided by popstate
		this.history.add( state.url, state );
	},

	// This binding is intended to catch the popstate events that are fired
	// when execution of the `$.navigate` method stops at window.location.hash = url;
	// and completely prevent them from propagating. The popstate event will then be
	// retriggered after execution resumes
	//
	// TODO grab the original event here and use it for the synthetic event in the
	//      second half of the navigate execution that will follow this binding
	popstate: function( event ) {
		var hash, state;

		// Partly to support our test suite which manually alters the support
		// value to test hashchange. Partly to prevent all around weirdness
		if ( !$.event.special.navigate.isPushStateEnabled() ) {
			return;
		}

		// If this is the popstate triggered by the actual alteration of the hash
		// prevent it completely. History is tracked manually
		if ( this.preventHashAssignPopState ) {
			this.preventHashAssignPopState = false;
			event.stopImmediatePropagation();
			return;
		}

		// if this is the popstate triggered after the `replaceState` call in the go
		// method, then simply ignore it. The history entry has already been captured
		if ( this.ignorePopState ) {
			this.ignorePopState = false;
			return;
		}

		// If there is no state, and the history stack length is one were
		// probably getting the page load popstate fired by browsers like chrome
		// avoid it and set the one time flag to false.
		// TODO: Do we really need all these conditions? Comparing location hrefs
		// should be sufficient.
		if ( !event.originalEvent.state &&
				this.history.stack.length === 1 &&
				this.ignoreInitialHashChange ) {
			this.ignoreInitialHashChange = false;

			if ( location.href === initialHref ) {
				event.preventDefault();
				return;
			}
		}

		// account for direct manipulation of the hash. That is, we will receive a popstate
		// when the hash is changed by assignment, and it won't have a state associated. We
		// then need to squash the hash. See below for handling of hash assignment that
		// matches an existing history entry
		// TODO it might be better to only add to the history stack
		//      when the hash is adjacent to the active history entry
		hash = path.parseLocation().hash;
		if ( !event.originalEvent.state && hash ) {
			// squash the hash that's been assigned on the URL with replaceState
			// also grab the resulting state object for storage
			state = this.squash( hash );

			// record the new hash as an additional history entry
			// to match the browser's treatment of hash assignment
			this.history.add( state.url, state );

			// pass the newly created state information
			// along with the event
			event.historyState = state;

			// do not alter history, we've added a new history entry
			// so we know where we are
			return;
		}

		// If all else fails this is a popstate that comes from the back or forward buttons
		// make sure to set the state of our history stack properly, and record the directionality
		this.history.direct( {
			id: ( event.originalEvent.state || {} ).id,
			url: ( event.originalEvent.state || {} ).url || hash,

			// When the url is either forward or backward in history include the entry
			// as data on the event object for merging as data in the navigate event
			present: function( historyEntry, direction ) {
				// make sure to create a new object to pass down as the navigate event data
				event.historyState = $.extend( {}, historyEntry );
				event.historyState.direction = direction;
			}
		} );
	},

	// NOTE must bind before `navigate` special event hashchange binding otherwise the
	//      navigation data won't be attached to the hashchange event in time for those
	//      bindings to attach it to the `navigate` special event
	// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
	//      broken (exception?)
	hashchange: function( event ) {
		var history, hash;

		// If hashchange listening is explicitly disabled or pushstate is supported
		// avoid making use of the hashchange handler.
		if ( !$.event.special.navigate.isHashChangeEnabled() ||
				$.event.special.navigate.isPushStateEnabled() ) {
			return;
		}

		// On occasion explicitly want to prevent the next hash from propagating because we only
		// with to alter the url to represent the new state do so here
		if ( this.preventNextHashChange ) {
			this.preventNextHashChange = false;
			event.stopImmediatePropagation();
			return;
		}

		history = this.history;
		hash = path.parseLocation().hash;

		// If this is a hashchange caused by the back or forward button
		// make sure to set the state of our history stack properly
		this.history.direct( {
			url: hash,

			// When the url is either forward or backward in history include the entry
			// as data on the event object for merging as data in the navigate event
			present: function( historyEntry, direction ) {
				// make sure to create a new object to pass down as the navigate event data
				event.hashchangeState = $.extend( {}, historyEntry );
				event.hashchangeState.direction = direction;
			},

			// When we don't find a hash in our history clearly we're aiming to go there
			// record the entry as new for future traversal
			//
			// NOTE it's not entirely clear that this is the right thing to do given that we
			//      can't know the users intention. It might be better to explicitly _not_
			//      support location.hash assignment in preference to $.navigate calls
			// TODO first arg to add should be the href, but it causes issues in identifying
			//      embedded pages
			missing: function() {
				history.add( hash, {
					hash: hash,
					title: document.title
				} );
			}
		} );
	}
} );

return $.mobile.Navigator;
} );
